// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Diagnostics;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Commands;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.System;

namespace Microsoft.CmdPal.UI;

public sealed partial class ListPage : Page,
    IRecipient<NavigateNextCommand>,
    IRecipient<NavigatePreviousCommand>,
    IRecipient<NavigateLeftCommand>,
    IRecipient<NavigateRightCommand>,
    IRecipient<NavigatePageDownCommand>,
    IRecipient<NavigatePageUpCommand>,
    IRecipient<ActivateSelectedListItemMessage>,
    IRecipient<ActivateSecondaryCommandMessage>
{
    private InputSource _lastInputSource;

    internal ListViewModel? ViewModel
    {
        get => (ListViewModel?)GetValue(ViewModelProperty);
        set => SetValue(ViewModelProperty, value);
    }

    // Using a DependencyProperty as the backing store for ViewModel.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ViewModelProperty =
        DependencyProperty.Register(nameof(ViewModel), typeof(ListViewModel), typeof(ListPage), new PropertyMetadata(null, OnViewModelChanged));

    private ListViewBase ItemView
    {
        get
        {
            return ViewModel?.IsGridView == true ? ItemsGrid : ItemsList;
        }
    }

    public ListPage()
    {
        this.InitializeComponent();
        this.NavigationCacheMode = NavigationCacheMode.Disabled;
        this.ItemView.Loaded += Items_Loaded;
        this.ItemView.PreviewKeyDown += Items_PreviewKeyDown;
        this.ItemView.PointerPressed += Items_PointerPressed;
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        if (e.Parameter is not AsyncNavigationRequest navigationRequest)
        {
            throw new InvalidOperationException($"Invalid navigation parameter: {nameof(e.Parameter)} must be {nameof(AsyncNavigationRequest)}");
        }

        if (navigationRequest.TargetViewModel is not ListViewModel listViewModel)
        {
            throw new InvalidOperationException($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(ListViewModel)}");
        }

        ViewModel = listViewModel;

        if (e.NavigationMode == NavigationMode.Back
            || (e.NavigationMode == NavigationMode.New && ItemView.Items.Count > 0))
        {
            // Upon navigating _back_ to this page, immediately select the
            // first item in the list
            ItemView.SelectedIndex = 0;
        }

        // RegisterAll isn't AOT compatible
        WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
        WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
        WeakReferenceMessenger.Default.Register<NavigateLeftCommand>(this);
        WeakReferenceMessenger.Default.Register<NavigateRightCommand>(this);
        WeakReferenceMessenger.Default.Register<NavigatePageDownCommand>(this);
        WeakReferenceMessenger.Default.Register<NavigatePageUpCommand>(this);
        WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
        WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);

        base.OnNavigatedTo(e);
    }

    protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
    {
        base.OnNavigatingFrom(e);

        WeakReferenceMessenger.Default.Unregister<NavigateNextCommand>(this);
        WeakReferenceMessenger.Default.Unregister<NavigatePreviousCommand>(this);
        WeakReferenceMessenger.Default.Unregister<NavigateLeftCommand>(this);
        WeakReferenceMessenger.Default.Unregister<NavigateRightCommand>(this);
        WeakReferenceMessenger.Default.Unregister<NavigatePageDownCommand>(this);
        WeakReferenceMessenger.Default.Unregister<NavigatePageUpCommand>(this);
        WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
        WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);

        if (ViewModel is not null)
        {
            ViewModel.PropertyChanged -= ViewModel_PropertyChanged;
            ViewModel.ItemsUpdated -= Page_ItemsUpdated;
        }

        if (e.NavigationMode != NavigationMode.New)
        {
            ViewModel?.SafeCleanup();
            CleanupHelper.Cleanup(this);
        }

        // Clean-up event listeners
        ViewModel = null;

        GC.Collect();
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
    private void Items_ItemClick(object sender, ItemClickEventArgs e)
    {
        if (e.ClickedItem is ListItemViewModel item)
        {
            if (_lastInputSource == InputSource.Keyboard)
            {
                ViewModel?.InvokeItemCommand.Execute(item);
                return;
            }

            var settings = App.Current.Services.GetService<SettingsModel>()!;
            if (settings.SingleClickActivates)
            {
                ViewModel?.InvokeItemCommand.Execute(item);
            }
            else
            {
                ViewModel?.UpdateSelectedItemCommand.Execute(item);
                WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
            }
        }
    }

    private void Items_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
    {
        if (ItemView.SelectedItem is ListItemViewModel vm)
        {
            var settings = App.Current.Services.GetService<SettingsModel>()!;
            if (!settings.SingleClickActivates)
            {
                ViewModel?.InvokeItemCommand.Execute(vm);
            }
        }
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
    private void Items_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        var vm = ViewModel;
        var li = ItemView.SelectedItem as ListItemViewModel;
        _ = Task.Run(() =>
        {
            vm?.UpdateSelectedItemCommand.Execute(li);
        });

        // There's mysterious behavior here, where the selection seemingly
        // changes to _nothing_ when we're backspacing to a single character.
        // And at that point, seemingly the item that's getting removed is not
        // a member of FilteredItems. Very bizarre.
        //
        // Might be able to fix in the future by stashing the removed item
        // here, then in Page_ItemsUpdated trying to select that cached item if
        // it's in the list (otherwise, clear the cache), but that seems
        // aggressively BODGY for something that mostly just works today.
        if (ItemView.SelectedItem is not null)
        {
            ItemView.ScrollIntoView(ItemView.SelectedItem);

            // Automation notification for screen readers
            var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemView);
            if (listViewPeer is not null && li is not null)
            {
                var notificationText = li.Title;

                UIHelper.AnnounceActionForAccessibility(
                    ItemsList,
                    notificationText,
                    "CommandPaletteSelectedItemChanged");
            }
        }
    }

    private void Items_RightTapped(object sender, RightTappedRoutedEventArgs e)
    {
        if (e.OriginalSource is FrameworkElement element &&
            element.DataContext is ListItemViewModel item)
        {
            if (ItemView.SelectedItem != item)
            {
                ItemView.SelectedItem = item;
            }

            ViewModel?.UpdateSelectedItemCommand.Execute(item);

            var pos = e.GetPosition(element);

            _ = DispatcherQueue.TryEnqueue(
                () =>
                {
                    WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
                        new OpenContextMenuMessage(
                            element,
                            Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.BottomEdgeAlignedLeft,
                            pos,
                            ContextMenuFilterLocation.Top));
                });
        }
    }

    private void Items_Loaded(object sender, RoutedEventArgs e)
    {
        // Find the ScrollViewer in the ItemView (ItemsList or ItemsGrid)
        var listViewScrollViewer = FindScrollViewer(this.ItemView);

        if (listViewScrollViewer is not null)
        {
            listViewScrollViewer.ViewChanged += ListViewScrollViewer_ViewChanged;
        }
    }

    private void ListViewScrollViewer_ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e)
    {
        var scrollView = sender as ScrollViewer;
        if (scrollView is null)
        {
            return;
        }

        // When we get to the bottom, request more from the extension, if they
        // have more to give us.
        // We're checking when we get to 80% of the scroll height, to give the
        // extension a bit of a heads-up before the user actually gets there.
        if (scrollView.VerticalOffset >= (scrollView.ScrollableHeight * .8))
        {
            ViewModel?.LoadMoreIfNeeded();
        }
    }

    public void Receive(NavigateNextCommand message)
    {
        // Note: We may want to just have the notion of a 'SelectedCommand' in our VM
        // And then have these commands manipulate that state being bound to the UI instead
        // We may want to see how other non-list UIs need to behave to make this decision
        // At least it's decoupled from the SearchBox now :)
        if (ViewModel?.IsGridView == true)
        {
            // For grid views, use spatial navigation (down)
            HandleGridArrowNavigation(VirtualKey.Down);
        }
        else
        {
            // For list views, use simple linear navigation
            if (ItemView.SelectedIndex < ItemView.Items.Count - 1)
            {
                ItemView.SelectedIndex++;
            }
            else
            {
                ItemView.SelectedIndex = 0;
            }
        }
    }

    public void Receive(NavigatePreviousCommand message)
    {
        if (ViewModel?.IsGridView == true)
        {
            // For grid views, use spatial navigation (up)
            HandleGridArrowNavigation(VirtualKey.Up);
        }
        else
        {
            // For list views, use simple linear navigation
            if (ItemView.SelectedIndex > 0)
            {
                ItemView.SelectedIndex--;
            }
            else
            {
                ItemView.SelectedIndex = ItemView.Items.Count - 1;
            }
        }
    }

    public void Receive(NavigateLeftCommand message)
    {
        // For grid views, use spatial navigation. For list views, just move up.
        if (ViewModel?.IsGridView == true)
        {
            HandleGridArrowNavigation(VirtualKey.Left);
        }
        else
        {
            // In list view, left arrow doesn't navigate
            // This maintains consistency with the SearchBar behavior
        }
    }

    public void Receive(NavigateRightCommand message)
    {
        // For grid views, use spatial navigation. For list views, just move down.
        if (ViewModel?.IsGridView == true)
        {
            HandleGridArrowNavigation(VirtualKey.Right);
        }
        else
        {
            // In list view, right arrow doesn't navigate
            // This maintains consistency with the SearchBar behavior
        }
    }

    public void Receive(ActivateSelectedListItemMessage message)
    {
        if (ViewModel?.ShowEmptyContent ?? false)
        {
            ViewModel?.InvokeItemCommand.Execute(null);
        }
        else if (ItemView.SelectedItem is ListItemViewModel item)
        {
            ViewModel?.InvokeItemCommand.Execute(item);
        }
    }

    public void Receive(ActivateSecondaryCommandMessage message)
    {
        if (ViewModel?.ShowEmptyContent ?? false)
        {
            ViewModel?.InvokeSecondaryCommandCommand.Execute(null);
        }
        else if (ItemView.SelectedItem is ListItemViewModel item)
        {
            ViewModel?.InvokeSecondaryCommandCommand.Execute(item);
        }
    }

    public void Receive(NavigatePageDownCommand message)
    {
        var indexes = CalculateTargetIndexPageUpDownScrollTo(true);
        if (indexes is null)
        {
            return;
        }

        if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
        {
            ItemView.SelectedIndex = indexes.Value.TargetIndex;
            ItemView.ScrollIntoView(ItemView.SelectedItem);
        }
    }

    public void Receive(NavigatePageUpCommand message)
    {
        var indexes = CalculateTargetIndexPageUpDownScrollTo(false);
        if (indexes is null)
        {
            return;
        }

        if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
        {
            ItemView.SelectedIndex = indexes.Value.TargetIndex;
            ItemView.ScrollIntoView(ItemView.SelectedItem);
        }
    }

    /// <summary>
    /// Calculates the item index to target when performing a page up or page down
    /// navigation. The calculation attempts to estimate how many items fit into
    /// the visible viewport by measuring actual container heights currently visible
    /// within the internal ScrollViewer. If measurements are not available a
    /// fallback estimate is used.
    /// </summary>
    /// <param name="isPageDown">True to calculate a page-down target, false for page-up.</param>
    /// <returns>
    /// A tuple containing the current index and the calculated target index, or null
    /// if a valid calculation could not be performed (for example, missing ScrollViewer).
    /// </returns>
    private (int CurrentIndex, int TargetIndex)? CalculateTargetIndexPageUpDownScrollTo(bool isPageDown)
    {
        var scroll = FindScrollViewer(ItemView);
        if (scroll is null)
        {
            return null;
        }

        var viewportHeight = scroll.ViewportHeight;
        if (viewportHeight <= 0)
        {
            return null;
        }

        var currentIndex = ItemView.SelectedIndex < 0 ? 0 : ItemView.SelectedIndex;
        var itemCount = ItemView.Items.Count;

        // Compute visible item heights within the ScrollViewer viewport
        const int firstVisibleIndexNotFound = -1;
        var firstVisibleIndex = firstVisibleIndexNotFound;
        var visibleHeights = new List<double>(itemCount);

        for (var i = 0; i < itemCount; i++)
        {
            if (ItemView.ContainerFromIndex(i) is FrameworkElement container)
            {
                try
                {
                    var transform = container.TransformToVisual(scroll);
                    var topLeft = transform.TransformPoint(new Point(0, 0));
                    var bottom = topLeft.Y + container.ActualHeight;

                    // If any part of the container is inside the viewport, consider it visible
                    if (topLeft.Y >= 0 && bottom <= viewportHeight)
                    {
                        if (firstVisibleIndex == firstVisibleIndexNotFound)
                        {
                            firstVisibleIndex = i;
                        }

                        visibleHeights.Add(container.ActualHeight > 0 ? container.ActualHeight : 0);
                    }
                }
                catch
                {
                    // ignore transform errors and continue
                }
            }
        }

        var itemsPerPage = 0;

        // Calculate how many items fit in the viewport based on their actual heights
        if (visibleHeights.Count > 0)
        {
            double accumulated = 0;
            for (var i = 0; i < visibleHeights.Count; i++)
            {
                accumulated += visibleHeights[i] <= 0 ? 1 : visibleHeights[i];
                itemsPerPage++;
                if (accumulated >= viewportHeight)
                {
                    break;
                }
            }
        }
        else
        {
            // fallback: estimate using first measured container height
            double itemHeight = 0;
            for (var i = currentIndex; i < itemCount; i++)
            {
                if (ItemView.ContainerFromIndex(i) is FrameworkElement { ActualHeight: > 0 } c)
                {
                    itemHeight = c.ActualHeight;
                    break;
                }
            }

            if (itemHeight <= 0)
            {
                itemHeight = 1;
            }

            itemsPerPage = Math.Max(1, (int)Math.Floor(viewportHeight / itemHeight));
        }

        var targetIndex = isPageDown
                              ? Math.Min(itemCount - 1, currentIndex + Math.Max(1, itemsPerPage))
                              : Math.Max(0, currentIndex - Math.Max(1, itemsPerPage));

        return (currentIndex, targetIndex);
    }

    private static void OnViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is ListPage @this)
        {
            if (e.OldValue is ListViewModel old)
            {
                old.PropertyChanged -= @this.ViewModel_PropertyChanged;
                old.ItemsUpdated -= @this.Page_ItemsUpdated;
            }

            if (e.NewValue is ListViewModel page)
            {
                page.PropertyChanged += @this.ViewModel_PropertyChanged;
                page.ItemsUpdated += @this.Page_ItemsUpdated;
            }
            else if (e.NewValue is null)
            {
                Logger.LogDebug("cleared view model");
            }
        }
    }

    // Called after we've finished updating the whole list for either a
    // GetItems or a change in the filter.
    private void Page_ItemsUpdated(ListViewModel sender, object args)
    {
        // If for some reason, we don't have a selected item, fix that.
        //
        // It's important to do this here, because once there's no selection
        // (which can happen as the list updates) we won't get an
        // ItemView_SelectionChanged again to give us another chance to change
        // the selection from null -> something. Better to just update the
        // selection once, at the end of all the updating.
        if (ItemView.SelectedItem is null)
        {
            ItemView.SelectedIndex = 0;
        }

        // Always reset the selected item when the top-level list page changes
        // its items
        if (!sender.IsNested)
        {
            ItemView.SelectedIndex = 0;
        }
    }

    private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        var prop = e.PropertyName;
        if (prop == nameof(ViewModel.FilteredItems))
        {
            Debug.WriteLine($"ViewModel.FilteredItems {ItemView.SelectedItem}");
        }
    }

    private static ScrollViewer? FindScrollViewer(DependencyObject parent)
    {
        if (parent is ScrollViewer viewer)
        {
            return viewer;
        }

        for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
        {
            var child = VisualTreeHelper.GetChild(parent, i);
            var result = FindScrollViewer(child);
            if (result is not null)
            {
                return result;
            }
        }

        return null;
    }

    // Find a logical neighbor in the requested direction using containers' positions.
    private void HandleGridArrowNavigation(VirtualKey key)
    {
        if (ItemView.Items.Count == 0)
        {
            // No items, goodbye.
            return;
        }

        var currentIndex = ItemView.SelectedIndex;
        if (currentIndex < 0)
        {
            // -1 is a valid value (no item currently selected)
            currentIndex = 0;
            ItemView.SelectedIndex = 0;
        }

        try
        {
            // Try to compute using container positions; if not available, fall back to simple +/-1.
            var currentContainer = ItemView.ContainerFromIndex(currentIndex) as FrameworkElement;
            if (currentContainer is not null && currentContainer.ActualWidth != 0 && currentContainer.ActualHeight != 0)
            {
                // Use center of current container as reference
                var curPoint = currentContainer.TransformToVisual(ItemView).TransformPoint(new Point(0, 0));
                var curCenterX = curPoint.X + (currentContainer.ActualWidth / 2.0);
                var curCenterY = curPoint.Y + (currentContainer.ActualHeight / 2.0);

                var bestScore = double.MaxValue;
                var bestIndex = currentIndex;

                for (var i = 0; i < ItemView.Items.Count; i++)
                {
                    if (i == currentIndex)
                    {
                        continue;
                    }

                    if (ItemView.ContainerFromIndex(i) is FrameworkElement c && c.ActualWidth > 0 && c.ActualHeight > 0)
                    {
                        var p = c.TransformToVisual(ItemView).TransformPoint(new Point(0, 0));
                        var centerX = p.X + (c.ActualWidth / 2.0);
                        var centerY = p.Y + (c.ActualHeight / 2.0);

                        var dx = centerX - curCenterX;
                        var dy = centerY - curCenterY;

                        var candidate = false;
                        var score = double.MaxValue;

                        switch (key)
                        {
                            case VirtualKey.Left:
                                if (dx < 0)
                                {
                                    candidate = true;
                                    score = Math.Abs(dy) + (Math.Abs(dx) * 0.7);
                                }

                                break;
                            case VirtualKey.Right:
                                if (dx > 0)
                                {
                                    candidate = true;
                                    score = Math.Abs(dy) + (Math.Abs(dx) * 0.7);
                                }

                                break;
                            case VirtualKey.Up:
                                if (dy < 0)
                                {
                                    candidate = true;
                                    score = Math.Abs(dx) + (Math.Abs(dy) * 0.7);
                                }

                                break;
                            case VirtualKey.Down:
                                if (dy > 0)
                                {
                                    candidate = true;
                                    score = Math.Abs(dx) + (Math.Abs(dy) * 0.7);
                                }

                                break;
                        }

                        if (candidate && score < bestScore)
                        {
                            bestScore = score;
                            bestIndex = i;
                        }
                    }
                }

                if (bestIndex != currentIndex)
                {
                    ItemView.SelectedIndex = bestIndex;
                    ItemView.ScrollIntoView(ItemView.SelectedItem);
                }

                return;
            }
        }
        catch
        {
            // ignore transform errors and fall back
        }

        // fallback linear behavior
        var fallback = key switch
        {
            VirtualKey.Left => Math.Max(0, currentIndex - 1),
            VirtualKey.Right => Math.Min(ItemView.Items.Count - 1, currentIndex + 1),
            VirtualKey.Up => Math.Max(0, currentIndex - 1),
            VirtualKey.Down => Math.Min(ItemView.Items.Count - 1, currentIndex + 1),
            _ => currentIndex,
        };
        if (fallback != currentIndex)
        {
            ItemView.SelectedIndex = fallback;
            ItemView.ScrollIntoView(ItemView.SelectedItem);
        }
    }

    private void Items_OnContextRequested(UIElement sender, ContextRequestedEventArgs e)
    {
        var (item, element) = e.OriginalSource switch
        {
            // caused by keyboard shortcut (e.g. Context menu key or Shift+F10)
            SelectorItem selectorItem => (ItemView.ItemFromContainer(selectorItem) as ListItemViewModel, selectorItem),

            // caused by right-click on the ListViewItem
            FrameworkElement { DataContext: ListItemViewModel itemViewModel } frameworkElement => (itemViewModel, frameworkElement),

            _ => (null, null),
        };

        if (item is null || element is null)
        {
            return;
        }

        if (ItemView.SelectedItem != item)
        {
            ItemView.SelectedItem = item;
        }

        if (!e.TryGetPosition(element, out var pos))
        {
            pos = new(0, element.ActualHeight);
        }

        _ = DispatcherQueue.TryEnqueue(
            () =>
            {
                WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
                    new OpenContextMenuMessage(
                        element,
                        Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.BottomEdgeAlignedLeft,
                        pos,
                        ContextMenuFilterLocation.Top));
            });
        e.Handled = true;
    }

    private void Items_OnContextCanceled(UIElement sender, RoutedEventArgs e)
    {
        _ = DispatcherQueue.TryEnqueue(() => WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>());
    }

    private void Items_PointerPressed(object sender, PointerRoutedEventArgs e) => _lastInputSource = InputSource.Pointer;

    private void Items_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
    {
        // Track keyboard as the last input source for activation logic.
        if (e.Key is VirtualKey.Enter or VirtualKey.Space)
        {
            _lastInputSource = InputSource.Keyboard;
            return;
        }

        // Handle arrow navigation when we're showing a grid.
        if (ViewModel?.IsGridView == true)
        {
            switch (e.Key)
            {
                case VirtualKey.Left:
                case VirtualKey.Right:
                case VirtualKey.Up:
                case VirtualKey.Down:
                    _lastInputSource = InputSource.Keyboard;
                    HandleGridArrowNavigation(e.Key);
                    e.Handled = true;
                    break;
            }
        }
    }

    private enum InputSource
    {
        None,
        Keyboard,
        Pointer,
    }
}
